package com.skynet.supervision.aop;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.ConsoleAppender;
import cn.hutool.core.text.StrFormatter;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.skynet.supervision.utils.TimeUtils;
import com.skynet.supervision.utils.UuidUtils;
import com.skynet.supervision.utils.functional.FuncUtils;
import lombok.Cleanup;
import lombok.val;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.ContentCachingResponseWrapper;
import reactor.core.publisher.Flux;

import javax.annotation.Nonnull;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.util.*;
import java.util.stream.Stream;

@WebFilter(filterName = "dump",urlPatterns = "/*")
@Component
@SuppressWarnings("unused")
public class HttpDumpFilter extends OncePerRequestFilter {

    interface LogStream {
        void write(String format, Object... arguments);

        void flush();

        void reset();

        boolean isEnable();
    }

    private final Logger logger;

//    @Value("${skynet.aop.dump.enable:false}")
    private Boolean enableHttpDump = true;


    public HttpDumpFilter() {
        this.logger = getHttpFilterLogger(HttpDumpFilter.class);
    }

    private static final List<MediaType> VISIBLE_TYPES = Arrays.asList(
            MediaType.valueOf("text/*"),
            MediaType.APPLICATION_FORM_URLENCODED,
            MediaType.APPLICATION_JSON,
            MediaType.APPLICATION_XML,
            MediaType.valueOf("application/*+json"),
            MediaType.valueOf("application/*+xml"),
            MediaType.MULTIPART_FORM_DATA
    );

    private static final String TimePattern = "yyyy-MM-dd HH:mm:ss.SSS";


    @Override
    protected void doFilterInternal(@Nonnull HttpServletRequest request,
                                    @Nonnull HttpServletResponse response,
                                    @Nonnull FilterChain filterChain) throws ServletException, IOException {
        if (Optional.ofNullable(enableHttpDump).orElse(true)){
            if (isAsyncDispatch(request)) {
                filterChain.doFilter(request, response);
            } else {
                doFilterWrapped(wrapRequest(request), wrapResponse(response), filterChain);
            }
        }else {
            filterChain.doFilter(request, response);
        }
    }


    protected void doFilterWrapped(ContentCachingRequestWrapper request,
                                   ContentCachingResponseWrapper response,
                                   FilterChain filterChain) throws ServletException, IOException {
        val logStream = new FastLogStream(logger);// new BulkLogStream(log);
        val logId = UuidUtils.randomUUID();
        try {
            beforeRequest(logStream, logId, request, response);
            filterChain.doFilter(request, response);
        } finally {
            afterRequest(logStream, logId, request, response);
            response.copyBodyToResponse();
            logStream.flush();
        }
    }

    protected void beforeRequest(LogStream logStream,
                                 String logId,
                                 ContentCachingRequestWrapper request,
                                 @SuppressWarnings("unused") ContentCachingResponseWrapper response) {
        if (logStream.isEnable()) {
            logRequestHeader(logStream, logId, request, request.getRemoteAddr() + "|>");
        }
    }

    protected void afterRequest(
            LogStream logStream,
            String logId,
            ContentCachingRequestWrapper request,
            ContentCachingResponseWrapper response) {
        if (logStream.isEnable()) {
            logRequestBody(logStream, request, request.getRemoteAddr() + "|>");
            logResponse(logStream, logId, response, request.getRemoteAddr() + "|<");
        }
    }

    private static void logRequestHeader(LogStream logStream,
                                         String logId,
                                         ContentCachingRequestWrapper request,
                                         String prefix) {
        val queryString = request.getQueryString();
        val method = request.getMethod();
        val reqUri = request.getRequestURI();
        logStream.write("{} {} started!  --------------- {} ---------------",
                prefix, TimeUtils.format(new Date(), TimePattern), logId);
        if (queryString == null) {
            logStream.write("{} {} {}", prefix, method, reqUri);
        } else {
            logStream.write("{} {} {}?{}", prefix, method, reqUri, queryString);
        }
        Collections.list(request.getHeaderNames())
                .forEach(headerName ->
                        Collections.list(request.getHeaders(headerName))
                                .forEach(headerValue ->
                                        logStream.write("{} {}: {}", prefix, headerName, headerValue)));
        logStream.write("{}", prefix);
    }

    private static void logRequestBody(LogStream logStream,
                                       ContentCachingRequestWrapper request,
                                       String prefix) {
        val content = request.getContentAsByteArray();
        if (content.length > 0) {
            logContent(logStream,
                    content,
                    request.getContentType(),
                    request.getCharacterEncoding(),
                    prefix);
        }
    }

    private static void logResponse(LogStream logStream,
                                    String logId,
                                    ContentCachingResponseWrapper response,
                                    String prefix) {
        val status = response.getStatus();
        logStream.write("{} {} {}", prefix, status, HttpStatus.valueOf(status).getReasonPhrase());
        Sets.newHashSet(response.getHeaderNames())
                .forEach(headerName ->
                        response.getHeaders(headerName)
                                .forEach(headerValue ->
                                        logStream.write("{} {}: {}",
                                                prefix, headerName, headerValue)));
        logStream.write("{}", prefix);
        val content = response.getContentAsByteArray();
        if (content.length > 0) {
            logContent(logStream, content, response.getContentType(), response.getCharacterEncoding(), prefix);
        }
        logStream.write("{} {} finished! --------------- {} --------------- ",
                prefix, TimeUtils.format(new Date(), TimePattern), logId);
    }

    private static final int kMaxLineSize = 2 * 1024;

    private static void logContent(LogStream logStream,
                                   byte[] content,
                                   String contentType,
                                   String contentEncoding,
                                   String prefix) {
        val readBuffer = new char[kMaxLineSize];
        val mediaType = MediaType.valueOf(contentType);
        val visible = VISIBLE_TYPES.stream().anyMatch(visibleType -> visibleType.includes(mediaType));
        if (visible) {
            try {
                @Cleanup val bis = new ByteArrayInputStream(content);
                //noinspection UnstableApiUsage
                Streams.stream(IOUtils.lineIterator(bis, contentEncoding))
                        .flatMap(line -> {
                            if (kMaxLineSize > line.length()) {
                                return Stream.of(line);
                            }
                            return longLineStream(line, readBuffer);
                        })
                        .forEach(line -> logStream.write("{} {}", prefix, line));
//            } catch (UnsupportedEncodingException e) {
//                logStream.write("{} [{} bytes content]", prefix, content.length);
            } catch (Exception e) {
                logStream.write("{} got error while perform log", prefix, e);
            }
        } else {
            logStream.write("{} [{} bytes content]", prefix, content.length);
        }
    }

    private static Stream<String> longLineStream(String longLine, char[] readBuffer) {
        return Flux.<String>create(FuncUtils.rethrowConsumer(
                emitter -> {
                    val reader = new StringReader(longLine);
                    for (; ; ) {
                        int nRead = reader.read(readBuffer);
                        if (nRead == -1) {
                            emitter.complete();
                            break;
                        }
                        emitter.next(new String(readBuffer));
                    }
                })).toStream();

    }

    private static ContentCachingRequestWrapper wrapRequest(HttpServletRequest request) {
        if (request instanceof ContentCachingRequestWrapper) {
            return (ContentCachingRequestWrapper) request;
        } else {
            return new ContentCachingRequestWrapper(request);
        }
    }

    private static ContentCachingResponseWrapper wrapResponse(HttpServletResponse response) {
        if (response instanceof ContentCachingResponseWrapper) {
            return (ContentCachingResponseWrapper) response;
        } else {
            return new ContentCachingResponseWrapper(response);
        }
    }

    @SuppressWarnings("SameParameterValue")
    private static <T> Logger getHttpFilterLogger(Class<T> clazz) {
        val lc = (LoggerContext) LoggerFactory.getILoggerFactory();
        val ple = new PatternLayoutEncoder();
        ple.setPattern("%msg%n");
        ple.setContext(lc);
        ple.start();
        val fileAppender = new ConsoleAppender<ILoggingEvent>();
        fileAppender.setEncoder(ple);
        fileAppender.setContext(lc);
        fileAppender.start();
        val logger = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(clazz);
        logger.addAppender(fileAppender);
        logger.setLevel(Level.INFO);
        logger.setAdditive(false); /* set to true if root should log too */
        return logger;
    }

    static class FastLogStream implements LogStream {
        private final Logger logger;

        FastLogStream(Logger logger) {
            this.logger = logger;
        }

        @Override
        public void write(String format, Object... arguments) {
            logger.info(format, arguments);
        }

        @Override
        public void flush() {
        }

        @Override
        public void reset() {
        }

        @Override
        public boolean isEnable() {
            return logger.isInfoEnabled();
        }
    }

    static class BulkLogStream implements LogStream {
        private StringBuilder sb;
        private final Logger logger;
        private boolean firstWriteIn;

        BulkLogStream(Logger logger) {
            this.sb = new StringBuilder();
            this.logger = logger;
            this.firstWriteIn = true;
        }

        @Override
        public boolean isEnable() {
            return logger.isInfoEnabled();
        }

        @Override
        public void write(String format, Object... arguments) {
            if (!isEnable()) {
                return;
            }
            if (firstWriteIn) {
                sb.append('\n');
                firstWriteIn = false;
            }
            val line = StrFormatter.format(format, arguments);
            sb.append(line);
            sb.append('\n');
        }

        @Override
        public void flush() {
            if (!isEnable()) {
                return;
            }
            logger.info(sb.toString());
        }

        @Override
        public void reset() {
            firstWriteIn = true;
            sb = new StringBuilder();
        }

    }

}
